/*******************************************************************************
 * Copyright (c) 2004, 2016 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * IBM - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.make.xlc.core.scannerconfig;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.eclipse.cdt.core.IMarkerGenerator;
import org.eclipse.cdt.core.ProblemMarkerInfo;
import org.eclipse.cdt.make.core.MakeCorePlugin;
import org.eclipse.cdt.make.internal.core.MakeMessages;
import org.eclipse.cdt.make.internal.core.scannerconfig.util.CCommandDSC;
import org.eclipse.cdt.make.internal.core.scannerconfig.util.KVStringPair;
import org.eclipse.cdt.make.internal.core.scannerconfig.util.SCDOptionsEnum;
import org.eclipse.cdt.make.internal.core.scannerconfig.util.TraceUtil;
import org.eclipse.cdt.make.xlc.core.scannerconfig.util.XLCCommandDSC;
import org.eclipse.cdt.utils.EFSExtensionManager;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;

/**
 * @author crecoskie
 *
 */
public class XLCBuildOutputParserUtility {
    protected class Problem {
        protected String description;
        protected IResource file;
        protected int lineNumber;
        protected int severity;
        protected String variableName;

        public Problem(IResource file, int lineNumber, String desciption, int severity, String variableName) {
            this.file = file;
            this.lineNumber = lineNumber;
            this.description = desciption;
            this.severity = severity;
            this.variableName = variableName;
        }
    }
    public static IPath convertCygpath(IPath path) {
    	if (path.segmentCount() > 1 && path.segment(0).equals("cygdrive")) { //$NON-NLS-1$
            StringBuilder buf = new StringBuilder(2);
            buf.append(Character.toUpperCase(path.segment(1).charAt(0)));
            buf.append(':');
            path = path.removeFirstSegments(2);
            path = path.setDevice(buf.toString());
            path = path.makeAbsolute();
        }
    	return path;
	}
    private List commandsList2;
    private int commandsN = 0;
    private List compiledFileList;

    private Map directoryCommandListMap;

    private IPath fBaseDirectory;
    private String fDefaultMacroDefinitionValue= "1"; //$NON-NLS-1$
    private Vector<IPath> fDirectoryStack;
    private ArrayList<Problem> fErrors;
    private int filesN = 0;

    private IMarkerGenerator fMarkerGenerator;

    private IProject project;

    private int workingDirsN = 0;

	/*
	 * For tracking the location of files being compiled
	 */
	private Map<String, IFile> fFilesInProject;
	private List<String> fCollectedFiles;
	private List<String> fNameConflicts;

	protected XLCBuildOutputParserUtility(IPath baseDirectory, IPath workingDirectory){
		fDirectoryStack = new Vector<IPath>();
        fErrors = new ArrayList<Problem>();
        this.fBaseDirectory = baseDirectory;
        if (workingDirectory != null) {
            pushDirectory(workingDirectory);
        }

	}
	/**
     *
     */
    public XLCBuildOutputParserUtility(IProject project, IPath workingDirectory,
                                              IMarkerGenerator markerGenerator) {
        fDirectoryStack = new Vector<IPath>();
        fErrors = new ArrayList<Problem>();
        this.project = project;
        fBaseDirectory = getPathForResource(project);
        if (workingDirectory != null) {
            pushDirectory(workingDirectory);
        }
    }

	private IPath getPathForResource(IResource resource) {
		// TODO: when the file system utility stuff is in, this will have to call it to get the path
		// for now, get the path from the URI
		URI locationURI = resource.getLocationURI();
		IPath path = new Path(locationURI.getPath());
		return path;
	}

    /**
     * Adds a mapping filename, generic_command
     * @param longFileName
     * @param genericLine
     */
    void addGenericCommandForFile(String longFileName, String genericCommand) {
        // if a file name has already been added once, return
        if (compiledFileList.contains(longFileName))
            return;
        compiledFileList.add(longFileName);

        String workingDir = getWorkingDirectory().toString();
        List directoryCommandList = (List) directoryCommandListMap.get(workingDir);
        if (directoryCommandList == null) {
            directoryCommandList = new ArrayList();
            directoryCommandListMap.put(workingDir, directoryCommandList);
            ++workingDirsN;
        }
        Map command21FileListMap = null;
        for (Iterator i = directoryCommandList.iterator(); i.hasNext(); ) {
            command21FileListMap = (Map) i.next();
            List fileList = (List) command21FileListMap.get(genericCommand);
            if (fileList != null) {
                if (!fileList.contains(longFileName)) {
                    fileList.add(longFileName);
                    ++filesN;
                }
                return;
            }
        }
        command21FileListMap = new HashMap(1);
        directoryCommandList.add(command21FileListMap);
        ++commandsN;
        List fileList = new ArrayList();
        command21FileListMap.put(genericCommand, fileList);
        fileList.add(longFileName);
        ++filesN;
    }

    /**
     * Adds a mapping command line -> file, this time without a dir
     * @param longFileName
     * @param genericLine
     */
    void addGenericCommandForFile2(String longFileName, String genericLine) {
        // if a file name has already been added once, return
        if (compiledFileList.contains(longFileName))
            return;
        compiledFileList.add(longFileName);

        String[] tokens = genericLine.split("\\s+"); //$NON-NLS-1$
        CCommandDSC command = getNewCCommandDSC(tokens, 0, false); // assume .c file type
        int index = commandsList2.indexOf(command);
        if (index == -1) {
            commandsList2.add(command);
            ++commandsN;
        }
        else {
            command = (CCommandDSC) commandsList2.get(index);
        }
//        // add a file
//        command.addFile(longFileName);
//        ++filesN;
    }

    public void changeMakeDirectory(String dir, int dirLevel, boolean enterDir) {
        if (enterDir) {
            /* Sometimes make screws up the output, so
             * "leave" events can't be seen.  Double-check level
             * here.
             */
            for (int parseLevel = getDirectoryLevel(); dirLevel < parseLevel; parseLevel = getDirectoryLevel()) {
                popDirectory();
            }
            pushDirectory(new Path(dir));
        } else {
            popDirectory();
            /* Could check to see if they match */
        }
    }

    /**
     * Called by the console line parsers to generate a problem marker.
     */
    public void generateMarker(IResource file, int lineNumber, String desc, int severity, String varName) {
        // No need to collect markers if marker generator is not present
        if (fMarkerGenerator != null) {
            Problem problem = new Problem(file, lineNumber, desc, severity, varName);
            fErrors.add(problem);
        }
    }

    /**
     *
     */
    void generateReport() {
        TraceUtil.metricsTrace("Stats for directory ", //$NON-NLS-1$
                   "Generic command: '", "' applicable for:",  //$NON-NLS-1$ //$NON-NLS-2$
                   directoryCommandListMap);
        TraceUtil.summaryTrace("Discovery summary", workingDirsN, commandsN, filesN); //$NON-NLS-1$
    }

    /**
     * @param filePath : String
     * @return filePath : IPath - not <code>null</code>
     */
    public IPath getAbsolutePath(String filePath) {
        IPath pFilePath;
        if (filePath.startsWith("/")) { //$NON-NLS-1$
        	return convertCygpath(new Path(filePath));
        }
        else if (filePath.startsWith("\\") || //$NON-NLS-1$
            (!filePath.startsWith(".") && //$NON-NLS-1$
             filePath.length() > 2 && filePath.charAt(1) == ':' &&
             (filePath.charAt(2) == '\\' || filePath.charAt(2) == '/'))) {
            // absolute path
            pFilePath = new Path(filePath);
        }
        else {
            // relative path
            IPath cwd = getWorkingDirectory();
            if (!cwd.isAbsolute()) {
                cwd = getBaseDirectory().append(cwd);
            }
            if (filePath.startsWith("`pwd`")) { //$NON-NLS-1$
            	if (filePath.length() > 5 && (filePath.charAt(5) == '/' || filePath.charAt(5) == '\\')) {
            		filePath = filePath.substring(6);
            	}
            	else {
            		filePath = filePath.substring(5);
            	}
            }
            pFilePath = cwd.append(filePath);
        }
        return pFilePath;
    }
    /**
     * @return Returns the fBaseDirectory.
     */
    public IPath getBaseDirectory() {
        return fBaseDirectory;
    }

    /**
     * Returns all CCommandDSC collected so far.
     * Currently this list is not filled, so it will always return an empty list.
     * @return List of CCommandDSC
     */
    public List getCCommandDSCList() {
        return new ArrayList(commandsList2);
    }

    protected int getDirectoryLevel() {
        return fDirectoryStack.size();
    }
    /**
     * @return Returns the fDirectoryStack.
     */
    protected Vector<IPath> getDirectoryStack() {
        return fDirectoryStack;
    }
    /**
     * @return Returns the fErrors.
     */
    protected ArrayList<Problem> getErrors() {
        return fErrors;
    }
	/**
     * @return Returns the fMarkerGenerator.
     */
    protected IMarkerGenerator getMarkerGenerator() {
        return fMarkerGenerator;
    }

    /**
     * @param genericLine
     * @param cppFileType
     * @return CCommandDSC compile command description
     */
    public CCommandDSC getNewCCommandDSC(String[] tokens, final int idxOfCompilerCommand, boolean cppFileType) {
		ArrayList dirafter = new ArrayList();
		ArrayList includes = new ArrayList();
        XLCCommandDSC command = new XLCCommandDSC(cppFileType, getProject());
        command.addSCOption(new KVStringPair(SCDOptionsEnum.COMMAND.toString(), tokens[idxOfCompilerCommand]));
        for (int i = idxOfCompilerCommand+1; i < tokens.length; ++i) {
        	String token = tokens[i];
        	//Target specific options: see GccScannerInfoConsoleParser
			if (token.startsWith("-m") ||		//$NON-NLS-1$
				token.equals("-ansi") ||		//$NON-NLS-1$
				token.equals("-posix") ||		//$NON-NLS-1$
				token.equals("-pthread") ||		//$NON-NLS-1$
				token.startsWith("-O") ||		//$NON-NLS-1$
				token.equals("-fno-inline") ||	//$NON-NLS-1$
				token.startsWith("-finline") ||	//$NON-NLS-1$
				token.equals("-fno-exceptions") ||	//$NON-NLS-1$
				token.equals("-fexceptions") ||		//$NON-NLS-1$
				token.equals("-fshort-wchar") ||	//$NON-NLS-1$
				token.equals("-fshort-double") ||	//$NON-NLS-1$
				token.equals("-fno-signed-char") ||	//$NON-NLS-1$
				token.equals("-fsigned-char") ||	//$NON-NLS-1$
				token.startsWith("-fabi-version=")	//$NON-NLS-1$
			) {
		        command.addSCOption(new KVStringPair(SCDOptionsEnum.COMMAND.toString(), token));
				continue;
        	}
            for (int j = SCDOptionsEnum.MIN; j <= SCDOptionsEnum.MAX; ++j) {
                final SCDOptionsEnum optionKind = SCDOptionsEnum.getSCDOptionsEnum(j);
				if (token.startsWith(optionKind.toString())) {
                    String option = token.substring(
                            optionKind.toString().length()).trim();
                    if (option.length() > 0) {
                        // ex. -I/dir
                    }
                    else if (optionKind.equals(SCDOptionsEnum.IDASH)) {
                    	for (Iterator iter=includes.iterator(); iter.hasNext(); ) {
                    		option = (String)iter.next();
                            KVStringPair pair = new KVStringPair(SCDOptionsEnum.IQUOTE.toString(), option);
                        	command.addSCOption(pair);
                    	}
                    	includes = new ArrayList();
                        // -I- has no parameter
                    }
                    else {
                        // ex. -I /dir
                        // take a next token
                        if (i+1 < tokens.length && !tokens[i+1].startsWith("-")) { //$NON-NLS-1$
                            option = tokens[++i];
                        }
                        else break;
                    }

                    if (option.length() > 0 && (
                            optionKind.equals(SCDOptionsEnum.INCLUDE) ||
                            optionKind.equals(SCDOptionsEnum.INCLUDE_FILE) ||
                            optionKind.equals(SCDOptionsEnum.IMACROS_FILE) ||
                            optionKind.equals(SCDOptionsEnum.IDIRAFTER) ||
                            optionKind.equals(SCDOptionsEnum.ISYSTEM) ||
                            optionKind.equals(SCDOptionsEnum.IQUOTE) )) {
                        option = (getAbsolutePath(option)).toString();
                    }

                    if (optionKind.equals(SCDOptionsEnum.IDIRAFTER)) {
                        KVStringPair pair = new KVStringPair(SCDOptionsEnum.INCLUDE.toString(), option);
                    	dirafter.add(pair);
                    }
                    else if (optionKind.equals(SCDOptionsEnum.INCLUDE)) {
                    	includes.add(option);
                    }
                    else { // add the pair
                    	if (optionKind.equals(SCDOptionsEnum.DEFINE)) {
                        	if (option.indexOf('=') == -1) {
                        		option += '='+ fDefaultMacroDefinitionValue;
                        	}
                    	}
                        KVStringPair pair = new KVStringPair(optionKind.toString(), option);
                    	command.addSCOption(pair);
                    }
                    break;
                }
            }
        }
        String option;
    	for (Iterator iter=includes.iterator(); iter.hasNext(); ) {
    		option = (String)iter.next();
            KVStringPair pair = new KVStringPair(SCDOptionsEnum.INCLUDE.toString(), option);
        	command.addSCOption(pair);
    	}
    	for (Iterator iter=dirafter.iterator(); iter.hasNext(); ) {
        	command.addSCOption((KVStringPair)iter.next());
    	}
        return command;
    }

    /**
     * @return Returns the project.
     */
    protected IProject getProject() {
        return project;
    }

    public IPath getWorkingDirectory() {
        if (fDirectoryStack.size() != 0) {
            return fDirectoryStack.lastElement();
        }
        // Fallback to the Project Location
        // FIXME: if the build did not start in the Project ?
        return fBaseDirectory;
    }

    protected IPath popDirectory() {
        int i = getDirectoryLevel();
        if (i != 0) {
            IPath dir = fDirectoryStack.lastElement();
            fDirectoryStack.removeElementAt(i - 1);
            return dir;
        }
        return new Path("");    //$NON-NLS-1$
    }

    protected void pushDirectory(IPath dir) {
        if (dir != null) {
            IPath pwd = null;
            if (fBaseDirectory != null && fBaseDirectory.isPrefixOf(dir)) {
                pwd = dir.removeFirstSegments(fBaseDirectory.segmentCount());
            } else {
                // check if it is a cygpath
            	pwd= convertCygpath(dir);
            }
            fDirectoryStack.addElement(pwd);
        }
    }

	public boolean reportProblems() {
        boolean reset = false;
        for (Iterator<Problem> iter = fErrors.iterator(); iter.hasNext(); ) {
            Problem problem = iter.next();
            if (problem.severity == IMarkerGenerator.SEVERITY_ERROR_BUILD) {
                reset = true;
            }
            if (problem.file == null) {
                fMarkerGenerator.addMarker(new ProblemMarkerInfo(
                    project,
                    problem.lineNumber,
                    problem.description,
                    problem.severity,
                    problem.variableName));
            } else {
                fMarkerGenerator.addMarker(new ProblemMarkerInfo(
                    problem.file,
                    problem.lineNumber,
                    problem.description,
                    problem.severity,
                    problem.variableName));
            }
        }
        fErrors.clear();
        return reset;
    }

    public void setDefaultMacroDefinitionValue(String val) {
    	if (val != null) {
    		fDefaultMacroDefinitionValue= val;
    	}
	}

    public String getDefaultMacroDefinitionValue() {
    	return fDefaultMacroDefinitionValue;
    }

	public String normalizePath(String path) {
		int column = path.indexOf(':');
		if (column > 0) {
			char driveLetter = path.charAt(column - 1);
			if (Character.isLowerCase(driveLetter)) {
				StringBuilder sb = new StringBuilder();
				if (column - 1 > 0) {
					sb.append(path.substring(0, column-1));
				}
				sb.append(Character.toUpperCase(driveLetter));
				sb.append(path.substring(column));
				path = sb.toString();
			}
		}
		if (path.indexOf('.') == -1 || path.equals(".")) {	//$NON-NLS-1$
			return (new Path(path)).toString();	// convert separators to '/'
		}
		// lose "./" segments since they confuse the Path normalization
		StringBuilder buf = new StringBuilder(path);
		int len = buf.length();
		StringBuilder newBuf = new StringBuilder(buf.length());
		int scp = 0; // starting copy point
		int ssp = 0;	// starting search point
		int sdot;
		boolean validPrefix;
		while (ssp < len && (sdot = buf.indexOf(".", ssp)) != -1) {	//$NON-NLS-1$
			validPrefix = false;
			int ddot = buf.indexOf("..", ssp);//$NON-NLS-1$
			if (sdot < ddot || ddot == -1) {
				newBuf.append(buf.substring(scp, sdot));
				scp = sdot;
				ssp = sdot + 1;
				if (ssp < len) {
					if (sdot == 0 || buf.charAt(sdot - 1) == '/' || buf.charAt(sdot - 1) == '\\') {
						validPrefix = true;
					}
					char nextChar = buf.charAt(ssp);
					if (validPrefix && nextChar == '/') {
						++ssp;
						scp = ssp;
					}
					else if (validPrefix && nextChar == '\\') {
						++ssp;
						if (ssp < len - 1 && buf.charAt(ssp) == '\\') {
							++ssp;
						}
						scp = ssp;
					}
					else {
						// no path delimiter, must be '.' inside the path
						scp = ssp - 1;
					}
				}
			}
			else if (sdot == ddot) {
				ssp = sdot + 2;
			}
		}
		newBuf.append(buf.substring(scp, len));

		IPath orgPath = new Path(newBuf.toString());
		return orgPath.toString();
	}


	/**
	 * Called by the console line parsers to find a file with a given name.
	 * @param fileName
	 * @return IFile or null
	 */
	public IFile findFile(String fileName) {
		IFile file = findFilePath(fileName);
		if (file == null) {
			// Try the project's map.
			file = findFileName(fileName);
			if (file != null) {
				// If there is a conflict then try all files in the project.
				if (isConflictingName(fileName)) {
					file = null;

					// Create a problem marker
					final String error = MakeMessages.getString("ConsoleParser.Ambiguous_Filepath_Error_Message"); //$NON-NLS-1$
					TraceUtil.outputError(error, fileName);
					generateMarker(getProject(), -1, error+fileName, IMarkerGenerator.SEVERITY_WARNING, null);
				}
			}
		}
		return file;
	}

	/**
	 * @param filePath
	 * @return
	 */
	protected IFile findFilePath(String filePath) {
		IPath path = null;
		IPath fp = new Path(filePath);
		if (fp.isAbsolute()) {
			if (getBaseDirectory().isPrefixOf(fp)) {
				int segments = getBaseDirectory().matchingFirstSegments(fp);
				path = fp.removeFirstSegments(segments);
			} else {
				path = fp;
			}
		} else {
			path = getWorkingDirectory().append(filePath);
		}

		IFile file = null;
		// The workspace may throw an IllegalArgumentException
		// Catch it and the parser should fallback to scan the entire project.
		try {
			file = findFileInWorkspace(path);
		} catch (Exception e) {
		}

		// We have to do another try, on Windows for cases like "TEST.C" vs "test.c"
		// We use the java.io.File canonical path.
		if (file == null || !file.exists()) {
			File f = path.toFile();
			try {
				String canon = f.getCanonicalPath();
				path = new Path(canon);
				file = findFileInWorkspace(path);
			} catch (IOException e1) {
			}
		}
		return (file != null && file.exists()) ? file : null;
	}

	/**
	 * @param fileName
	 * @return
	 */
	protected IFile findFileName(String fileName) {
		IPath path = new Path(fileName);
		return fFilesInProject.get(path.lastSegment());
	}

	protected IFile findFileInWorkspace(IPath path) {
		IFile file = null;
		if (path.isAbsolute()) {
			IWorkspaceRoot root = getProject().getWorkspace().getRoot();

			// construct a URI, based on the project's locationURI, that points
			// to the given path
			URI projectURI = project.getLocationURI();

			URI newURI = EFSExtensionManager.getDefault().createNewURIFromPath(projectURI, path.toString());

			IFile[] files = root.findFilesForLocationURI(newURI);

			for (int i = 0; i < files.length; i++) {
				if (files[i].getProject().equals(getProject())) {
					file = files[i];
					break;
				}
			}

		} else {
			file = getProject().getFile(path);
		}
		return file;
	}

	protected void collectFiles(IContainer parent, List result) {
		try {
			IResource[] resources = parent.members();
			for (int i = 0; i < resources.length; i++) {
				IResource resource = resources[i];
				if (resource instanceof IFile) {
					result.add(resource);
				} else if (resource instanceof IContainer) {
					collectFiles((IContainer) resource, result);
				}
			}
		} catch (CoreException e) {
			MakeCorePlugin.log(e.getStatus());
		}
	}

	protected boolean isConflictingName(String fileName) {
		IPath path = new Path(fileName);
		return fNameConflicts.contains(path.lastSegment());
	}

	public List translateRelativePaths(IFile file, String fileName, List includes) {
		List translatedIncludes = new ArrayList(includes.size());
		for (Iterator i = includes.iterator(); i.hasNext(); ) {
			String include = (String) i.next();
			IPath includePath = new Path(include);
			if (!includePath.isAbsolute() && !includePath.isUNC()) {	// do not translate UNC paths
				// First try the current working directory
				IPath cwd = getWorkingDirectory();
				if (!cwd.isAbsolute()) {
					cwd = getBaseDirectory().append(cwd);
				}

				IPath filePath = new Path(fileName);
				if (!filePath.isAbsolute()) {
					// check if the cwd is the right one
					// appending fileName to cwd should yield file path
					filePath = cwd.append(fileName);
				}
				if (!filePath.toString().equalsIgnoreCase(file.getLocation().toString())) {
					// must be the cwd is wrong
					// check if file name starts with ".."
					if (fileName.startsWith("..")) {	//$NON-NLS-1$
						// probably multiple choices for cwd, hopeless
						final String error = MakeMessages.getString("ConsoleParser.Working_Directory_Error_Message"); //$NON-NLS-1$
						TraceUtil.outputError(error, fileName);
						generateMarker(file, -1, error,	 IMarkerGenerator.SEVERITY_WARNING, fileName);
						break;
					}
					else {
						// remove common segments at the end
						IPath tPath = new Path(fileName);
						if (fileName.startsWith(".")) {	//$NON-NLS-1$
							tPath = tPath.removeFirstSegments(1);
						}
						// get the file path from the file
						filePath = file.getLocation();
						IPath lastFileSegment = filePath.removeFirstSegments(filePath.segmentCount() - tPath.segmentCount());
						if (lastFileSegment.matchingFirstSegments(tPath) == tPath.segmentCount()) {
							cwd = filePath.removeLastSegments(tPath.segmentCount());
						}
					}
				}

				IPath candidatePath = cwd.append(includePath);
				File dir = candidatePath.toFile();
				include = candidatePath.toString();
				if (!dir.exists()) {
					final String error = MakeMessages.getString("ConsoleParser.Nonexistent_Include_Path_Error_Message"); //$NON-NLS-1$
					TraceUtil.outputError(error, include);
//					generateMarker(file, -1, error+include, IMarkerGenerator.SEVERITY_WARNING, fileName);
				}
			}
			// TODO VMIR for now add unresolved paths as well
			translatedIncludes.add(include);
		}
		return translatedIncludes;
	}
}
